/*
 * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.engine.impl.bpmn.deployer;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.Process;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.ProcessEngineConfiguration;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.DeploymentEntity;
import org.activiti.engine.impl.persistence.entity.IdentityLinkEntity;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntityManager;
import org.activiti.engine.task.IdentityLinkType;
import org.apache.commons.lang3.StringUtils;

/**
 * Methods for working with deployments.  Much of the actual work of {@link BpmnDeployer} is
 * done by orchestrating the different pieces of work this class does; by having them here,
 * we allow other deployers to make use of them.
 */
public class BpmnDeploymentHelper {

    protected TimerManager timerManager;
    protected EventSubscriptionManager eventSubscriptionManager;

    /**
     * Verifies that no two process definitions share the same key, to prevent database unique
     * index violation.
     *
     * @throws ActivitiException if any two processes have the same key
     */
    public void verifyProcessDefinitionsDoNotShareKeys(Collection<ProcessDefinitionEntity> processDefinitions) {
        Set<String> keySet = new LinkedHashSet<String>();
        for (ProcessDefinitionEntity processDefinition : processDefinitions) {
            if (keySet.contains(processDefinition.getKey())) {
                throw new ActivitiException(
                    "The deployment contains process definitions with the same key (process id attribute), this is not allowed"
                );
            }
            keySet.add(processDefinition.getKey());
        }
    }

    /**
     * Updates all the process definition entities to match the deployment's values for tenant,
     * engine version, and deployment id.
     */
    public void copyDeploymentValuesToProcessDefinitions(
        DeploymentEntity deployment,
        List<ProcessDefinitionEntity> processDefinitions
    ) {
        String engineVersion = deployment.getEngineVersion();
        String tenantId = deployment.getTenantId();
        String deploymentId = deployment.getId();

        for (ProcessDefinitionEntity processDefinition : processDefinitions) {
            // Backwards compatibility
            if (engineVersion != null) {
                processDefinition.setEngineVersion(engineVersion);
            }

            // process definition inherits the tenant id
            if (tenantId != null) {
                processDefinition.setTenantId(tenantId);
            }

            processDefinition.setDeploymentId(deploymentId);
        }
    }

    /**
     * Updates all the process definition entities to have the correct resource names.
     */
    public void setResourceNamesOnProcessDefinitions(ParsedDeployment parsedDeployment) {
        for (ProcessDefinitionEntity processDefinition : parsedDeployment.getAllProcessDefinitions()) {
            String resourceName = parsedDeployment.getResourceForProcessDefinition(processDefinition).getName();
            processDefinition.setResourceName(resourceName);
        }
    }

    /**
     * Gets the most recent persisted process definition that matches this one for tenant and key.
     * If none is found, returns null.  This method assumes that the tenant and key are properly
     * set on the process definition entity.
     */
    public ProcessDefinitionEntity getMostRecentVersionOfProcessDefinition(ProcessDefinitionEntity processDefinition) {
        String key = processDefinition.getKey();
        String tenantId = processDefinition.getTenantId();
        ProcessDefinitionEntityManager processDefinitionManager = Context.getCommandContext()
            .getProcessEngineConfiguration()
            .getProcessDefinitionEntityManager();

        ProcessDefinitionEntity existingDefinition = null;

        if (tenantId != null && !tenantId.equals(ProcessEngineConfiguration.NO_TENANT_ID)) {
            existingDefinition = processDefinitionManager.findLatestProcessDefinitionByKeyAndTenantId(key, tenantId);
        } else {
            existingDefinition = processDefinitionManager.findLatestProcessDefinitionByKey(key);
        }

        return existingDefinition;
    }

    /**
     * Gets the persisted version of the already-deployed process definition.  Note that this is
     * different from {@link #getMostRecentVersionOfProcessDefinition} as it looks specifically for
     * a process definition that is already persisted and attached to a particular deployment,
     * rather than the latest version across all deployments.
     */
    public ProcessDefinitionEntity getPersistedInstanceOfProcessDefinition(ProcessDefinitionEntity processDefinition) {
        String deploymentId = processDefinition.getDeploymentId();
        if (StringUtils.isEmpty(processDefinition.getDeploymentId())) {
            throw new IllegalStateException("Provided process definition must have a deployment id.");
        }

        ProcessDefinitionEntityManager processDefinitionManager = Context.getCommandContext()
            .getProcessEngineConfiguration()
            .getProcessDefinitionEntityManager();
        ProcessDefinitionEntity persistedProcessDefinition = null;
        if (
            processDefinition.getTenantId() == null ||
            ProcessEngineConfiguration.NO_TENANT_ID.equals(processDefinition.getTenantId())
        ) {
            persistedProcessDefinition = processDefinitionManager.findProcessDefinitionByDeploymentAndKey(
                deploymentId,
                processDefinition.getKey()
            );
        } else {
            persistedProcessDefinition = processDefinitionManager.findProcessDefinitionByDeploymentAndKeyAndTenantId(
                deploymentId,
                processDefinition.getKey(),
                processDefinition.getTenantId()
            );
        }

        return persistedProcessDefinition;
    }

    /**
     * Updates all timers and events for the process definition.  This removes obsolete message and signal
     * subscriptions and timers, and adds new ones.
     */
    public void updateTimersAndEvents(
        ProcessDefinitionEntity processDefinition,
        ProcessDefinitionEntity previousProcessDefinition,
        ParsedDeployment parsedDeployment
    ) {
        Process process = parsedDeployment.getProcessModelForProcessDefinition(processDefinition);
        BpmnModel bpmnModel = parsedDeployment.getBpmnModelForProcessDefinition(processDefinition);

        eventSubscriptionManager.removeObsoleteMessageEventSubscriptions(previousProcessDefinition);
        eventSubscriptionManager.addMessageEventSubscriptions(processDefinition, process, bpmnModel);

        eventSubscriptionManager.removeObsoleteSignalEventSubScription(previousProcessDefinition);
        eventSubscriptionManager.addSignalEventSubscriptions(
            Context.getCommandContext(),
            processDefinition,
            process,
            bpmnModel
        );

        timerManager.removeObsoleteTimers(processDefinition);
        timerManager.scheduleTimers(processDefinition, process);
    }

    public void disableExistingStartEventSubscriptions() {
        timerManager.removeExistingTimerStartEventJobs();
        eventSubscriptionManager.removeExistingSignalStartEventSubscriptions();
        eventSubscriptionManager.removeExistingMessageStartEventSubscriptions();
    }

    protected enum ExpressionType {
        USER,
        GROUP,
    }

    /**
     * @param processDefinition
     */
    public void addAuthorizationsForNewProcessDefinition(Process process, ProcessDefinitionEntity processDefinition) {
        CommandContext commandContext = Context.getCommandContext();

        if (process != null) {
            addAuthorizationsFromIterator(
                commandContext,
                process.getCandidateStarterUsers(),
                processDefinition,
                ExpressionType.USER
            );
            addAuthorizationsFromIterator(
                commandContext,
                process.getCandidateStarterGroups(),
                processDefinition,
                ExpressionType.GROUP
            );
        }
    }

    protected void addAuthorizationsFromIterator(
        CommandContext commandContext,
        List<String> expressions,
        ProcessDefinitionEntity processDefinition,
        ExpressionType expressionType
    ) {
        if (expressions != null) {
            Iterator<String> iterator = expressions.iterator();
            while (iterator.hasNext()) {
                @SuppressWarnings("cast")
                String expression = iterator.next();
                IdentityLinkEntity identityLink = commandContext.getIdentityLinkEntityManager().create();
                identityLink.setProcessDef(processDefinition);
                if (expressionType.equals(ExpressionType.USER)) {
                    identityLink.setUserId(expression);
                } else if (expressionType.equals(ExpressionType.GROUP)) {
                    identityLink.setGroupId(expression);
                }
                identityLink.setType(IdentityLinkType.CANDIDATE);
                commandContext.getIdentityLinkEntityManager().insert(identityLink);
            }
        }
    }

    public TimerManager getTimerManager() {
        return timerManager;
    }

    public void setTimerManager(TimerManager timerManager) {
        this.timerManager = timerManager;
    }

    public EventSubscriptionManager getEventSubscriptionManager() {
        return eventSubscriptionManager;
    }

    public void setEventSubscriptionManager(EventSubscriptionManager eventSubscriptionManager) {
        this.eventSubscriptionManager = eventSubscriptionManager;
    }
}
